using System;
using System.Collections;
using System.Collections.Generic;
using System.Web;
using System.Xml;
using System.Xml.XPath;
using Com.FastSearch.Esp.Search;
using Com.FastSearch.Esp.Search.Result;
using Com.FastSearch.SharePoint.Interfaces;
using Com.FastSearch.SharePoint.WebControls.ESPSearchWebParts.UI;
using Microsoft.SharePoint.Utilities;
using System.IO;

namespace Com.FastSearch.SharePoint.BL.SearchManager
{
    class ESPSearchResult : IESPSearchResult
    {
        #region Constants

        private const string FAST_QT_NAVIGATION = "FastQT_Navigation";
        private const string FAST_FEATURED_CONTENT = "Features";

        #endregion

        #region Private variables

        private readonly IDictionary<string, IESPNavigator> _navigators;
        private readonly List<IESPDocumentSummary> _Items;
        private XmlDocument xmlResult;
        private XmlDocument xmlSearchStatistics;
        private XmlDocument xmlDidYouMean;
        private XmlDocument xmlFeaturedContent;
        private IQueryTransformation featuredContent;
        private readonly string timeToSearch;
        private ArrayList suggestions;
        private ArrayList modifications;

        #endregion

        public ESPSearchResult(IQueryResult result, IESPSearchSettings searchSettings)
        {
            if (result != null && searchSettings != null)
            {
                int from = searchSettings.Start1 + 1;
                int to = searchSettings.Start1 + searchSettings.NumberOfResultsPerPage;
                _Items = new List<IESPDocumentSummary>();
                DocCount = result.DocCount;

                to = Math.Min(to, DocCount);
                if (result.DocCount > 0)
                {

                    for (int i = from; i <= to; i++)
                    {
                        IDocumentSummary doc = result.GetDocument(i);
                        _Items.Add(new ESPDocumentSummary(doc));
                    }

                    /*for (IEnumerator documents = result.Documents(); documents.MoveNext(); )
                    {
                        IDocumentSummary doc = (IDocumentSummary)documents.Current;
                        _Items.Add(new ESPDocumentSummary(doc));
                    }*/
                }

                IDictionary<string, IList<IESPNavigatorItem>> appliedValues = ExtractAppliedValues(result, searchSettings);
                _navigators = new Dictionary<string, IESPNavigator>(result.NavigatorCount());
                foreach (INavigator nav in result.Navigators())
                {
                    _navigators[nav.Name] = new ESPNavigator(nav, appliedValues.ContainsKey(nav.Modifier) ? appliedValues[nav.Modifier] : new List<IESPNavigatorItem>(0));
                }

                ExtractDidYouMeanData(result);
                ExtractFeaturedContentData(result);

                timeToSearch = result.TimeUsedAsString;
            }
        }

        private void ExtractDidYouMeanData(IQueryResult result)
        {
            IQueryTransformations transforms = result.GetQueryTransformations(false);
            suggestions = new ArrayList(transforms.GetSuggestions(Constants.DID_YOU_MEAN));
            modifications = new ArrayList(transforms.GetModifications(Constants.DID_YOU_MEAN));
        }

        private void ExtractFeaturedContentData(IQueryResult result)
        {
            featuredContent = null;

            IQueryTransformations transforms = result.GetQueryTransformations(false);
            ICollection allTransformations = transforms.AllQueryTransformations;
            foreach (IQueryTransformation transformation in allTransformations)
            {
                if (FAST_FEATURED_CONTENT.Equals(transformation.Name))
                {
                    featuredContent = transformation;
                    break; // because there will always be only one Features Transformation
                }
            }
        }

        #region IESPSearchResult Members

        public int DocCount
        {
            get;
            private set;
        }

        public long MaxRank
        {
            get;
            private set;
        }

        public string TimeToSearch
        {
            get { return timeToSearch; }
        }

        public List<IESPDocumentSummary> Items
        {
            get
            {
                return _Items;
            }
        }

        public string[] NavigatorNames
        {
            get
            {
                string[] navigatorNames = new string[_navigators.Count];
                _navigators.Keys.CopyTo(navigatorNames, 0);
                return navigatorNames;
            }
        }

        public IESPNavigator[] Navigators
        {
            get
            {
                if (_navigators == null)
                {
                    return new IESPNavigator[0];
                }
                IESPNavigator[] navigators = new IESPNavigator[_navigators.Count];
                _navigators.Values.CopyTo(navigators, 0);
                return navigators;
            }
        }

        public IXPathNavigable XmlResult
        {
            get
            {
                if (xmlResult==null)
                {
                    xmlResult = new XmlDocument();
                    BuildXMLResultDom(xmlResult);
                }
                return xmlResult;
              }
        }

        /// <summary>
        /// If this property is set to true the xml result object will have the
        /// debugmode attribute in the ALL_RESULTS XML Node set to true.
        /// </summary>
        private bool _debugMode = false;
        public bool DebugMode
        {
            set
            {
                _debugMode = value;
            }
            get
            {
                return _debugMode;
            }
        }

        /// <summary>
        /// Build the xml dom object of the resultset.
        /// The xml dom is based on the MS XML Result DOM used for displaying results in Sharepoint.
        /// </summary>
        /// <param name="doc"></param>
        private void BuildXMLResultDom(XmlDocument doc)
        {
            XmlNode root = doc.AppendChild(doc.CreateElement("All_Results"));

            if (DebugMode)
            {
                XmlAttribute xmlAttr = doc.CreateAttribute("debugmode");
                xmlAttr.Value = "true";
                root.Attributes.Append(xmlAttr);
            }

            if (DocCount == 0)
            {
                // Add original field
                XmlElement elem = CreateElement(doc, "NoResults", "No Results found for this query!");
                root.AppendChild(elem);

            } else {

                foreach (IESPDocumentSummary Item in Items)
                {
                    AddResultXML(doc, root, Item);
                }
            }         
        }



        /// <summary>
        /// Add an IEPSDocumentSummary to the resulting XML Result document.
        /// Basically converts the IESPDocumentSummary to a MS Result XML Document, including the hithighlighting.
        ///
        /// </summary>
        /// <param name="doc">XML result document which contains the search results</param>
        /// <param name="root">Root node of the XML result document</param>
        /// <param name="Item">IESPDocumentsummary wich contains 1 result hit from the search</param>
        private static void AddResultXML(XmlDocument doc, XmlNode root, IESPDocumentSummary Item)
        {
            ESPToMSSearchHelper helper = ESPToMSSearchHelper.Instance;
            XmlElement node = doc.CreateElement("Result");
            foreach (string key in Item.Values.AllKeys)
            {
                string keyMS = key;

                // Add original field
                // The field are NOT HtmlEncoded.
                // You might want to make sure that's done in the xslt...
                XmlElement elem = CreateElement(doc, keyMS, Item.Values[key]);                    
                node.AppendChild(elem);

                // If mapping is available also add the mapped field
                if (helper.ESPtoMSMapping.ContainsKey(key))
                {
                    keyMS = helper.ESPtoMSMapping[key];

                    // Special case for description as this is the fallback if no highliting could be done
                    // and we want to make sure it's html encoded......
                    if (keyMS.Equals("description"))
                    {
                        elem = CreateElement(doc, keyMS, SPEncode.HtmlEncode( Item.Values[key]));
                    }
                    else
                    {
                        elem = CreateElement(doc, keyMS, Item.Values[key]);
                    }                    
                    
                        //SPEncode.HtmlEncode(Item.Values[key]));
                    node.AppendChild(elem);
                }
            }

            // Add Highlighted xml elements
            string bodyProperty = "body";
            string titleProperty = "title";
            string urlProperty = "url";

            try
            {
                AddHithighlightedsummary(doc, node, SPEncode.HtmlEncode(Item.Values[bodyProperty]));
            } catch (Exception ex)
            {
                // In case the building of the hithighlithing failes the
                // default body text will shown as is.
                Console.WriteLine("Could not build the hithighlithing: " + ex.Message);
            }
            try
            {
                AddHithighlightedproperties(doc, node, SPEncode.HtmlEncode(Item.Values[titleProperty]), Item.Values[urlProperty]);
            }
            catch (Exception ex)
            {
                // In case the building of the hithighlithing failes the
                // default body text will shown as is.
                Console.WriteLine("Could not build the hithighlithing: " + ex.Message);
            }

            // Add Image mappings dependen on content type
            string imageurl = helper.GetImageUrlForContent(Item.Values["contenttype"]);
            XmlElement imgelem = CreateElement(doc, "imageurl", imageurl, "imageurldescription",
                CreateImageUrlDescription(Item.Values["format"]));
            node.AppendChild(imgelem);

            root.AppendChild(node);
        }

        /// <summary>
        /// Build the highlited summary nodes to be conform to the MS result XML document.
        /// This nodes are used to build the hit highlighting for the shown body text.
        /// As ESP is doing the hithighlighting as part of the search we replace the actual &gt;b&lt; and
        /// &gt;b/&lt; with &gt;co&lt;
        ///
        /// Example:
        /// &gt;b>Welcome&gt;/b&lt;  &gt;b>Welcome&gt;/b&lt; to FAST
        /// &gt;hithighlightedsummary&lt;
        /// &gt;c0>Welcome&gt;/c0&lt;  &gt;c0>Welcome&gt;/c0&lt; to FAST
        /// &gt;/hithighlightedsummary&lt;
        ///
        /// </summary>
        /// <param name="doc">XML result document which contains the search results </param>
        /// <param name="node">parent node where the hithighliting node shall be added</param>
        /// <param name="s">body/teaser text from the ESP search result</param>
        private static void AddHithighlightedsummary(XmlDocument doc, XmlNode node, string s)
        {
            // Split string and create the hithighlightedsummary xml nodes
            if (!string.IsNullOrEmpty(s))
            {
                string highlited = s.Replace("&lt;b&gt;", "<c0>").Replace("&lt;/b&gt;", "</c0>").Replace("</c0> <c0>", "</c0><space/><c0>");
                XmlDocument xmlDoc = new XmlDocument();
                xmlDoc.LoadXml("<hithighlightedsummary>" + highlited + "</hithighlightedsummary>");

                XmlNodeList nodeList = xmlDoc.SelectNodes("hithighlightedsummary");
                //XmlNode summaryNode = doc.CreateElement("hithighlightedsummary");

                AddRecursiveXmlNodes(doc, node, nodeList);
                //node.AppendChild(summaryNode);
            }
        }

        /// <summary>
        /// Recursively add the xml nodes for document highlighting from the manually built xml tree
        /// to the result xml document.
        /// It is not possible to copy nodes from one document to another. So a "manual" copy is done instead.
        ///
        /// </summary>
        /// <param name="doc">Result XML Document where the nodes shall be copied to</param>
        /// <param name="parentNode">Parent node to which the xml nodes are copied to</param>
        /// <param name="nodes">The nodes which shall be copied </param>
        private static void AddRecursiveXmlNodes(XmlDocument doc, XmlNode parentNode, XmlNodeList nodes)
        {
            foreach (XmlNode xmlNode in nodes)
            {
                XmlNode newNode;
                if (xmlNode.NodeType == XmlNodeType.Text)
                {
                    newNode = doc.CreateTextNode( xmlNode.Value);
                } else
                {
                    newNode = doc.CreateElement(xmlNode.Name);
                }
                if (xmlNode.HasChildNodes)
                {
                    AddRecursiveXmlNodes(doc, newNode, xmlNode.ChildNodes);
                }
                parentNode.AppendChild(newNode);
            }
        }

        /// <summary>
        /// This method builds the hithighlighting for the title.
        /// As ESP is doing the hithighlighting as part of the search we replace the actual &gt;b&lt; and
        /// &gt;b/&lt; with &gt;co&lt;
        /// Example xml sctruct after conversion:
        ///
        ///&gt;hithighlightedproperties&lt;
        ///&gt;HHTitle&lt;
        ///&gt;c0&lt;Title&gt;/c0&lt;  &gt;c0>Highlited&gt;/c0&lt; some more title  &gt;c0&lt;Welcome&gt;/c0&lt;
        ///&gt;/HHTitle&lt;
        ///&gt;HHUrl&lt;http://osl2t214&gt;/HHUrl&lt;
        ///&gt;/hithighlightedproperties&lt;
        ///
        /// </summary>
        ///  <param name="doc">XML result document which contains the search results </param>
        /// <param name="node">parent node where the hithighliting node shall be added</param>
        /// <param name="title">title text from ESP Search Result</param>
        /// <param name="url">url from the ESP Search Result</param>
        private static void AddHithighlightedproperties(XmlDocument doc, XmlNode node, string title, string url)
        {
            if (!string.IsNullOrEmpty(title))
            {
                string highlited = title.Replace("&lt;b&gt;", "<c0>").Replace("&lt;/b&gt;", "</c0>").Replace("</c0> <c0>", "</c0><space/><c0>");
                XmlDocument xmlDoc = new XmlDocument();
                xmlDoc.LoadXml("<titlesummary>" + highlited + "</titlesummary>");
                XmlNodeList nodeList = xmlDoc.SelectNodes("titlesummary");
                XmlNode summaryNode = doc.CreateElement("hithighlightedproperties");
                XmlNode titleNode = doc.CreateElement("HHTitle");
                XmlNode urlNode = doc.CreateElement("HHUrl");
                urlNode.InnerText = url;
                summaryNode.AppendChild(urlNode);
                AddRecursiveXmlNodes(doc, titleNode, nodeList);
                summaryNode.AppendChild(titleNode);
                node.AppendChild(summaryNode);
            }
        }

        /// <summary>
        /// Create a hoover description for the image.
        ///
        /// </summary>
        /// <param name="format">Document format in human readeable form</param>
        /// <returns>Summary of the Document format in extended human readeable form </returns>
        private static string CreateImageUrlDescription(String format)
        {
            return "Result of type:" + format;
        }


        /// <summary>
        /// Create a xml result node element for the given result property and value
        /// The content will be embadded in a cdata structure
        /// </summary>
        /// <param name="doc">XML result document which contains the search results</param>
        /// <param name="title">Parameter name</param>
        /// <param name="value">Parameter value</param>
        /// <returns>XmlElement of the name/value pair.</returns>
        ///
        private static XmlElement CreateElement(XmlDocument doc, string title, string value)
        {
            return CreateElement(doc, title, value, null, null);
        }


        /// <summary>
        /// Create a xml result node element for the given result property and value with an additional attribute name and value
        /// The content will be embadded in a cdata structure.
        /// If the atribute name is null, no attribute is added to the node
        /// </summary>
        /// <param name="doc">XML result document which contains the search results</param>
        /// <param name="param">Parameter name</param>
        /// <param name="value">Parameter value</param>
        /// <returns>XmlElement of the name/value pair.</returns>
        /// <param name="attrName">Attribute name</param>
        /// <param name="attrValue">Attribute value</param>
        /// <returns>XmlElement of the name/value pair with the additional provided attribute name and value</returns>
        private static XmlElement CreateElement(XmlDocument doc, string param, string value, string attrName, string attrValue)
        {
            XmlElement elem = doc.CreateElement(param);
            if (attrName != null)
            {
                XmlAttribute attr = doc.CreateAttribute(attrName);
                attr.Value = attrValue;
                elem.Attributes.Append(attr);
            }
            XmlCDataSection cData = doc.CreateCDataSection(value);
            elem.AppendChild(cData);
            //elem.InnerText = value;
            return elem;
        }


        public IXPathNavigable GetXmlDidYouMean(
            HttpRequest request,
            ESPSearchGroupEnum espSearchGroup,
            IESPSearchSettings searchSettings)
        {
            if (xmlDidYouMean == null)
            {
                NavigationHelperParameter navParameter = new NavigationHelperParameter(request.RawUrl, request.QueryString, request.FilePath);
                xmlDidYouMean = new XmlDocument();
                BuildDidYouMeanXml(xmlDidYouMean, navParameter, espSearchGroup, searchSettings);
            }
            return xmlDidYouMean;
        }

        private void BuildDidYouMeanXml(XmlDocument doc, NavigationHelperParameter navHelperParam,
            ESPSearchGroupEnum  espSearchGroup,
            IESPSearchSettings searchSettings)
        {
            XmlNode root = doc.AppendChild(doc.CreateElement("DidYouMean"));
            foreach (IQueryTransformation suggestion in suggestions)
            {
                if (suggestion.MessageID == 14)
                {
                    XmlElement elem = doc.CreateElement("Suggestion");
                    XmlElement orig = CreateElement(doc, "OriginalQuery", suggestion.Custom);
                    elem.AppendChild(orig);
                    XmlElement modified = CreateElement(doc, "SuggestedQuery", suggestion.Query);
                    elem.AppendChild(modified);

                    XmlElement modifiedUri = CreateElement(doc, "SuggestedQueryUri",
                                                           NavigationHelper.SetParameter(navHelperParam,
                                                                                         searchSettings.QueryURLPropertyName,
                                                                                         suggestion.Query,
                                                                                         espSearchGroup));
                    elem.AppendChild(modifiedUri);
                    root.AppendChild(elem);
                }
            }

            foreach (IQueryTransformation modification in modifications)
            {
                if (modification.MessageID == 13)
                {
                    XmlElement elem = doc.CreateElement("Modification");
                    XmlElement orig = CreateElement(doc, "OriginalQuery", modification.Custom);
                    elem.AppendChild(orig);
                    XmlElement modified = CreateElement(doc, "ModifiedQuery", modification.Query);
                    elem.AppendChild(modified);
                    root.AppendChild(elem);
                }
            }
        }

        public IXPathNavigable GetXmlFeaturedContent(
            HttpRequest request,
            ESPSearchGroupEnum espSearchGroup,
            IESPSearchSettings searchSettings)
        {
            if (xmlFeaturedContent == null)
            {
                NavigationHelperParameter navParameter = new NavigationHelperParameter(request.RawUrl, request.QueryString, request.FilePath);
                xmlFeaturedContent = new XmlDocument();
                BuildFeaturedContentXml(xmlFeaturedContent, navParameter, espSearchGroup, searchSettings);
            }

            return xmlFeaturedContent;
        }

        // retrieve the FeaturedContent items from the Query Result.
        private void BuildFeaturedContentXml(XmlDocument doc, NavigationHelperParameter navHelperParam,
            ESPSearchGroupEnum espSearchGroup,
            IESPSearchSettings searchSettings)
        {
            if (featuredContent != null)
            {
                String featuresXml = featuredContent.Custom;
                featuresXml = featuresXml.Replace("&lt;", "<");
                featuresXml = featuresXml.Replace("&gt;", ">");
                featuresXml = featuresXml.Replace("&quot;", "\"");
                featuresXml = featuresXml.Replace("&amp;", "&");

                doc.Load(new StringReader(featuresXml));
                return;
            }

            doc.AppendChild(doc.CreateElement("Features"));
        }

        public IESPNavigator GetNavigator(string navigatorName)
        {
            return _navigators.ContainsKey(navigatorName) ? _navigators[navigatorName] : null;
        }

        public IXPathNavigable GetXmlSearchStatistics(int offset, int hitsperpage)
        {
            if (xmlSearchStatistics == null)
            {
                if (hitsperpage - 1 > DocCount)
                {
                    hitsperpage = DocCount + 1;
                }
                xmlSearchStatistics = new XmlDocument();
                BuildSearchStatisticsXml(xmlSearchStatistics, offset + 1, hitsperpage - 1, DocCount, timeToSearch);
            }
            return xmlSearchStatistics;
        }

        internal static void BuildSearchStatisticsXml(XmlDocument doc, int offset, int hitsperpage, int totalresults, string timeToSearch)
        {
            int to = hitsperpage + offset;

            if (offset > totalresults)
            {
                offset = totalresults;
            }

            if (to > totalresults)
            {
                to = totalresults;
            }

            XmlNode root = doc.AppendChild(doc.CreateElement("Statistics"));

            XmlElement elem = doc.CreateElement("from");
            elem.InnerText = offset.ToString();
            root.AppendChild(elem);
            elem = doc.CreateElement("to");
            elem.InnerText = to.ToString();
            root.AppendChild(elem);
            elem = doc.CreateElement("total");
            elem.InnerText = totalresults.ToString();
            root.AppendChild(elem);
            elem = doc.CreateElement("timeused");
            elem.InnerText = timeToSearch;
            root.AppendChild(elem);
        }

        #endregion

        internal static IDictionary<string, IList<IESPNavigatorItem>> ExtractAppliedValues(IQueryResult result, IESPSearchSettings searchSettings)
        {
            IDictionary<string, IList<IESPNavigatorItem>> appliedValues = new Dictionary<string, IList<IESPNavigatorItem>>();
            char[] split = new char[] { searchSettings.NavSplitCharacter };
            ExtractAppliedValues(appliedValues, searchSettings.Nav, split);
            ExtractAppliedValues(appliedValues, searchSettings.ScopeNav, split);
            return appliedValues;
        }

        internal static void ExtractAppliedValues(IDictionary<string, IList<IESPNavigatorItem>> appliedValues, string[] modifications, char[] split)
        {
            if (modifications != null)
            {
                foreach (string modification in modifications)
                {
                    string[] parts = modification.Split(split);
                    if (parts.Length == 4)
                    {
                        AddValue(appliedValues, parts[0], parts[1], parts[2], parts[3]);
                    }
                }
            }
        }

        private static void AddValue(IDictionary<string, IList<IESPNavigatorItem>> appliedValues, string field, string value, string displayValue, string context)
        {
            if (!appliedValues.ContainsKey(field))
            {
                appliedValues.Add(field, new List<IESPNavigatorItem>(4));
            }
            appliedValues[field].Add(new ESPNavigatorItem(field, value, displayValue, context, 0));
        }
    }
}
